/*
 *   SlimPlayer Copyright (c) 2009 Koelec
 *
 *   This file is part of SlimPlayer plugin for SageTv.
 *
 *   Slimplayer is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *   Slimplayer is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with SoftSqueeze; if not, write to the Free Software
 *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 */
package nl.cmkprojects.sage.slimplayer;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.swing.JApplet;

import nl.cmkprojects.sage.slimplayer.squeeze.CliConnection;
import nl.cmkprojects.sage.slimplayer.squeeze.CliListener;
import nl.cmkprojects.sage.slimplayer.squeeze.CliMessage;
import nl.cmkprojects.sage.slimplayer.squeeze.CliParameterIterator;
import nl.cmkprojects.sage.web.ArtistInfo;
import nl.cmkprojects.sage.web.ArtistInfoRetriever;
import nl.cmkprojects.sage.web.ArtistNews;
import nl.cmkprojects.sage.web.ArtistNewsRetriever;
import nl.cmkprojects.sage.web.WebHarvestArtistInfoRetriever;
import nl.cmkprojects.sage.web.WebHarvestArtistNewsRetriever;

import org.apache.commons.collections.map.LRUMap;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.titmuss.softsqueeze.Softsqueeze;
import org.titmuss.softsqueeze.audio.AudioDecoder;
import org.titmuss.softsqueeze.audio.AudioMixer;
import org.titmuss.softsqueeze.config.Config;
import org.titmuss.softsqueeze.net.ProtocolListener;
import org.titmuss.softsqueeze.platform.Platform;

public class SlimPlayerImpl implements CliListener, ProtocolListener {
  private static final String SLIMPLAYER = "slimplayer/";

  private static final String MACADDRESS = "macaddress";

  private static final String AUDIO_MP3DECODER = "audio.mp3decoder";

  private static final String AUDIO_MIXER = "audio.mixer";

  private static final String SHOW_STATION = "showstation";

  private static SlimPlayerImpl instance;

  private Favorite[] cachedFavorites = new Favorite[] {};

  private static final int LIST_SIZE = 256;

  private static final int CACHE_SIZE = 100;

  private Softsqueeze squeeze;

  private String displayLine0 = "";

  private String displayLine1 = "";

  private LRUMap songInfoCache = new LRUMap(CACHE_SIZE);

  private CliCon cmdCli;

  private CliConnection listenCli;

  private int cliport = 9090;

  private int webport = 9000;

  private String slimserver = "localhost";

  private enum AudioCollaboration {
    SYNC, INDEPENDENT, EXCLUSIVE
  }

  private static final String LOOPBACK_DEVICE_INPUT = "slimplayer/loopbackDeviceInput";

  private static final String AUDIO_COLLABORATION_PROPERTY_NAME = "slimplayer/audioCollaborationMode";

  private static final String LOOPBACK_STATIONID = "slimplayer/loopbackStationID";

  private static final String DISPLAY_VIDEO_ON_MENUS_PROP = "display_video_on_menus";

  private static final String DISPLAY_VIDEO_ON_MENUS_XNEVER = "XNever";

  private static final String SLIM_SERVER = "slimserver";

  // private static final String WATCH_BUFFER_SIZE_PROP =
  // "slimplayer/liveBufferSize";

  private String displayVideoOnMenus = DISPLAY_VIDEO_ON_MENUS_XNEVER;

  private enum PlayMode {
    PLAY, STOP
  }

  private PlayMode playMode = PlayMode.STOP;

  private enum ConnectionState {
    CONNECTED, DISCONNECTED
  }

  private ConnectionState state = ConnectionState.DISCONNECTED;

  int offsetNewsScroll = 0;

  private static Logger logger = Logger.getLogger(SlimPlayerImpl.class);

  private String playerid = null;

  private ServerState serverState = new ServerState();

  private ArtistInfoManager artistInfoManager = new ArtistInfoManager();

  private int mutedVolume = 100;

  private String lastNewsItemUri = "";
  private String lastNewsItem = "";

  private String uiContext;

  private AutoTransitionTimerTask transitionTimerTask;

  private String stviVersion;

  private String tickerFiller;

  public static SlimPlayerImpl getInstance() {
    logger.debug("getInstance()");
    try {
      if (instance == null) {
        instance = new SlimPlayerImpl("", "");
      } else {
        // if (!instance.isPowerOn()) {
        // instance.powerOn();
        // }
      }
    } catch (Exception e) {
      logger.error("getInstance()", e);
    }
    return instance;
  }

  public static SlimPlayerImpl getInstance(String uiContext) {
    logger.debug("getInstance(UIContext=)" + uiContext);
    try {
      if (instance == null) {
        instance = new SlimPlayerImpl(uiContext, "unknown");
      }
    } catch (Exception e) {
      logger.error("getInstance()", e);
    }
    return instance;
  }

  public static SlimPlayerImpl getInstance(String uiContext, String stviVersion) {
    logger.debug("getInstance(UIContext=)" + uiContext);
    try {
      if (instance == null) {
        instance = new SlimPlayerImpl(uiContext, stviVersion);
      }
    } catch (Exception e) {
      logger.error("getInstance()", e);
    }
    return instance;
  }

  private void startWatchDog(final String uiContext) {
    Thread t = new Thread(new Runnable() {
      public void run() {
        while (true) {
          Socket socket = null;
          try {
            socket = new Socket(slimserver, cliport);
            state = ConnectionState.CONNECTED;
            connect();
            logger.info("slimserver connection established");
            SageTVApi.refreshGui(uiContext);
            try {
              socket.getInputStream().read();
            } catch (Exception e) {
            }
            state = ConnectionState.DISCONNECTED;
            logger.warn("slimserver connection lost, retrying every 5 seconds");
            cmdCli.disconnect();
            clearInfo();
            SageTVApi.refreshGui(uiContext);
          } catch (Exception e) {
          } finally {
            try {
              if (socket != null) {
                socket.close();
              }
            } catch (IOException e) {
              logger.error("failed to close socket", e);
            }
          }
          try {
            Thread.sleep(5000); // retry in 5 seconds
          } catch (InterruptedException e1) {
          }
        }
      }
    });
    t.start();
  }

  /*
   * (non-Javadoc)
   * 
   * @see nl.cmkprojects.sage.slimplayer.ISlimPlayer#getSlimProperty(java.lang.
   * String, java.lang.String)
   */
  public String getSlimProperty(String key, String def) {
    return SagePropertyHelper.getSageTvProperty(key, def);
  }

  /*
   * (non-Javadoc)
   * 
   * @see nl.cmkprojects.sage.slimplayer.ISlimPlayer#setSlimProperty(java.lang.
   * String, java.lang.String)
   */
  public void setSlimProperty(String key, String val) {
    SagePropertyHelper.setSageTvProperty(key, val);
  }

  public String[] getAudioMixers() {
    return AudioMixer.getJavaSoundMixers();
  }

  public String getAudioMixer() {
    return getOverridenProperty(AUDIO_MIXER);
  }

  private String getOverridenProperty(String key) {
    String val = getSlimProperty(SLIMPLAYER + key, null);
    if (val == null) {
      val = Config.getProperty(key);
    }
    return val;
  }

  private void setOverridenProperty(String key, String val) {
    Config.putProperty(key, val);
    setSlimProperty(SLIMPLAYER + key, val);
  }

  public void setAudioMixer(String mixer) {
    setOverridenProperty(AUDIO_MIXER, mixer);
  }

  public String[] getAudioDecoders() {
    return AudioDecoder.getMP3Decoders();
  }

  public String getAudioDecoder() {
    return getOverridenProperty(AUDIO_MP3DECODER);
  }

  public void setAudioDecoder(String decoder) {
    setOverridenProperty(AUDIO_MP3DECODER, decoder);
  }

  public String getSlimHostName() {
    return getOverridenProperty(SLIM_SERVER);
  }

  public void setSlimHostName(String name) {

    setOverridenProperty(SLIM_SERVER, name);
  }

  public String getMacaddress() {
    return getOverridenProperty(MACADDRESS);
  }

  public void setMacaddress(String val) {
    setOverridenProperty(MACADDRESS, val);
  }

  public boolean isConnected() {
    return state == ConnectionState.CONNECTED;
  }

  public void connect() {
    logger.debug("connect()");
    synchronized (cmdCli) {
      if (!cmdCli.isConnected() && isConnected()) {
        try {
          initSqueeze();
        } catch (Exception e) {
          logger.error("failed to connect to slimserver: " + slimserver, e);
        }
      }
    }
  }

  private String getUsername() {
    return getSlimProperty("slimplayer/username", "");
  }

  private String getPassword() {
    return getSlimProperty("slimplayer/password", "");
  }

  private void initSqueeze() throws Exception {
    logger.debug("initSqueeze()");
    if (squeeze == null) {
      cmdCli.connect(InetAddress.getByName(slimserver), cliport);
      cmdCli.logon(getUsername(), getPassword());
      JApplet app = new JApplet();
      squeeze = new Softsqueeze(app);
      listenCli = new CliConnection();
      listenCli.connect(InetAddress.getByName(slimserver), cliport);
      CliMessage cmd = new CliMessage(playerid, "login " + getUsername() + " " + getPassword());
      listenCli.queueMessage(this, cmd);

      registerCliListener();
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
      }
      // make sure softsqueeze has already connected; softsqueeze is auto
      // connecting and
      // protocollistener collection in Protocol class is not threadsafe
      squeeze.getProtocol().addProtocolListener("strm", this);
    } else {
      cmdCli.connect(InetAddress.getByName(slimserver), cliport);
      cmdCli.logon(getUsername(), getPassword());
      // listenCli.connect(InetAddress.getByName(slimserver), cliport);
      CliMessage cmd = new CliMessage(playerid, "login " + getUsername() + " " + getPassword());
      listenCli.queueMessage(this, cmd);

      // we need to reregister to make sure the listen 1 command is send
      // again,
      // because we lost the connection
      registerCliListener();
    }
  }

  private SlimPlayerImpl(String uiContext, String stviVersion) {
    this.uiContext = uiContext;
    StringBuffer sb = new StringBuffer();
    for (int i = 0; i < 300; i++) {
      sb.append(" ");
    }
    tickerFiller = sb.toString();

    System.setProperty("skins", "headless");
    Platform.init();
    Config.init();

    overrideConfig(MACADDRESS);
    overrideConfig(AUDIO_MIXER);
    overrideConfig(AUDIO_MP3DECODER);
    overrideConfig(SLIM_SERVER);

    System.out.println(getVersionInfo());
    // playerid = Platform.macAddressToString(Platform.getMacAddress());
    playerid = Config.getProperty(MACADDRESS);
    this.stviVersion = stviVersion;
    // Config.putProperty("skin", "headless");
    String value = Config.getProperty(SLIM_SERVER);
    if (value != null && value.length() > 0) {
      slimserver = value;
    } else {
      Config.setSlimNetworkServer(slimserver);
    }
    int port = Config.getIntegerProperty("cliport");
    if (port != -1) {
      cliport = port;
    }
    cmdCli = new CliCon(playerid, isSqueezeCenterVersion(), LIST_SIZE, slimserver, webport);
    startWatchDog(uiContext);
  }

  private void overrideConfig(String key) {
    String val = getSlimProperty(SLIMPLAYER + key, null);
    if (val != null) {
      Config.putProperty(key, val);
    }
  }

  private void clearInfo() {
    serverState = new ServerState();
  }

  /*
   * (non-Javadoc)
   * 
   * @seenl.cmkprojects.sage.slimplayer.ISlimPlayer#play(nl.cmkprojects.sage.
   * slimplayer.Playable)
   */
  public void play(Playable reference) {
    logger.debug("play " + reference);
    startSagePlayback();
    cmdCli.play(reference);
    startNowPlayingMenuTransitionTimer();
  }

  /*
   * (non-Javadoc)
   * 
   * @see nl.cmkprojects.sage.slimplayer.ISlimPlayer#stopPlayback()
   */
  public void stopPlayback() {
    cmdCli.stopPlayback();
    if (getAudioCollaborationMode().equals(AudioCollaboration.SYNC)) {
      SageTVApi.stopSageTvMediaPlayer(uiContext);
    }
    stopNowPlayingMenuTransitionTimer();
  }

  /*
   * Stops playback and switches the uicontext to the specified one.
   */
  public void stopPlayback(String uiContext) {
    stopPlayback();
    this.uiContext = uiContext;
  }

  public String getUIContextName() {
    return uiContext;
  }

  private void startSagePlayback() {
    if (getAudioCollaborationMode().equals(AudioCollaboration.SYNC)) {
      logger.debug("col-mode=sync");
      String loopbackDevice = getLoopbackDeviceInput();
      String viewingDevice = SageTVApi.getCaptureDeviceInputBeingViewed(uiContext);
      logger.debug("viewing device input = " + viewingDevice + ", loopback device input = " + loopbackDevice);
      if (loopbackDevice != null && !loopbackDevice.equals(viewingDevice)) {
        // long watchBufferSize =
        // Long.parseLong(getSlimProperty(WATCH_BUFFER_SIZE_PROP,
        // "50000000"));
        // logger.debug("buffersize = " + watchBufferSize);
        // SageTVApi.watchLive(uiContext, device,
        // watchBufferSize);
        SageTVApi.watchShow(uiContext, loopbackDevice, getLoopbackStationID());
      }
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see nl.cmkprojects.sage.slimplayer.ISlimPlayer#startPlayback()
   */
  public void startPlayback() {
    startSagePlayback();
    // cmdCli.startPlayback();
    ir("768910ef");
    startNowPlayingMenuTransitionTimer();
  }

  public void delete(Playable reference) {
    logger.debug("delete " + reference);
    cmdCli.delete(reference);
  }

  /*
   * (non-Javadoc)
   * 
   * @see nl.cmkprojects.sage.slimplayer.ISlimPlayer#getArtistBio()
   */
  public String getArtistBio() {
    ArtistInfo artistInfo = getArtistInfo();
    if (artistInfo != null) {
      logger.debug("artistBio=" + artistInfo.getBio());
      return artistInfo.getBio();
    }
    return "";
  }

  private ArtistInfo getArtistInfo() {
    return artistInfoManager.getArtistInfo(serverState.getArtist(), serverState.getTitle());
  }

  /*
   * (non-Javadoc)
   * 
   * @see nl.cmkprojects.sage.slimplayer.ISlimPlayer#getArtistName()
   */
  public String getArtistName() {
    if (serverState != null) {
      logger.debug("artistName=" + serverState.getArtist());
      return serverState.getArtist();
    }
    return "";
  }

  /*
   * (non-Javadoc)
   * 
   * @see nl.cmkprojects.sage.slimplayer.ISlimPlayer#getArtistNews()
   */
  public ArtistNews getArtistNews() {
    ArtistInfo artistInfo = getArtistInfo();
    if (artistInfo != null) {
      return artistInfo.getNews();
    }
    return null;
  }


  /*
   * (non-Javadoc)
   * 
   * @see nl.cmkprojects.sage.slimplayer.ISlimPlayer#getArtistLatestNews2(int,
   * int)
   */
  public String getArtistLatestNews3(int size, int increment) {
    String full = getArtistLatestNews();
    if (full != null && full.length() > 0) {
      full = getArtistLatestNewsSource() + " - " + full;
      full = tickerFiller + full + tickerFiller;
      full = full.replaceAll("\n", " ");
      if (full.length() > size) {
        int start = offsetNewsScroll + increment;
        offsetNewsScroll += increment;
        int end = start + size;
        if (end > full.length()) {
          end = full.length();
          offsetNewsScroll = 0;
        }
        return full.substring(start, end);
      }
    }
    return "";
  }

  /*
   * (non-Javadoc)
   * 
   * @see nl.cmkprojects.sage.slimplayer.ISlimPlayer#getArtistNewsItem(java.lang
   * .String)
   */
  public String getArtistNewsItem(String uri) {
    String result = "";
    if (uri == null) {
      return "";
    }
    if (lastNewsItemUri.equals(uri)) {
      return lastNewsItem;
    }
    ArtistNewsRetriever newsRetriever = new WebHarvestArtistNewsRetriever();
    result = newsRetriever.getNewsItem(uri);
    lastNewsItem = result;
    lastNewsItemUri = uri;
    return result;
  }

  /*
   * (non-Javadoc)
   * 
   * @see
   * nl.cmkprojects.sage.slimplayer.ISlimPlayer#getArtistNews(java.lang.String )
   */
  public ArtistNews getArtistNews(String artist) {
    ArtistNewsRetriever newsRetriever = new WebHarvestArtistNewsRetriever();
    return newsRetriever.getNews(artist);
  }

  /*
   * (non-Javadoc)
   * 
   * @see nl.cmkprojects.sage.slimplayer.ISlimPlayer#getArtistLatestNewsSource()
   */
  public String getArtistLatestNewsSource() {
    ArtistInfo artistInfo = getArtistInfo();
    if (artistInfo != null) {
      ArtistNews news = artistInfo.getNews();
      if (news.getItemRefList().size() > 0) {
        return news.getItemRefList().get(0).getSource();
      }
    }
    return "";
  }

  /*
   * (non-Javadoc)
   * 
   * @see nl.cmkprojects.sage.slimplayer.ISlimPlayer#getArtistLatestNews()
   */
  public String getArtistLatestNews() {
    ArtistInfo artistInfo = getArtistInfo();
    if (artistInfo != null) {
      ArtistNews news = artistInfo.getNews();
      if (news.getItemRefList().size() > 0) {
        return news.getItemRefList().get(0).getHeadline() + " - " + news.getItemRefList().get(0).getText();
      }
    }
    return "";
  }

  public String getArtistPictureUrl() {
    ArtistInfo artistInfo = getArtistInfo();
    if (artistInfo != null) {
      List<String> photos = artistInfo.getPhotos();
      return (photos.isEmpty() ? "" : photos.get(0));
    }
    return "";
  }

  public String getNextArtistPictureUrl() {
    ArtistInfo artistInfo = getArtistInfo();
    if (artistInfo != null) {
      String photo = artistInfo.getNextPhoto();
      if (StringUtils.isNotEmpty(photo)) {
        return photo;
      }
      if (StringUtils.isNotEmpty(artistInfo.getPictureURL())) {
        return artistInfo.getPictureURL();
      }
    }
    return "";
  }

  public String getCurrentCoverartUrl() {
    return String.format("http://%s:%d/music/current/cover.jpg?player=%s", slimserver, webport, playerid);
  }

  /*
   * (non-Javadoc)
   * 
   * @see nl.cmkprojects.sage.slimplayer.ISlimPlayer#getCurrentStation()
   */
  public String getCurrentStation() {
    return serverState.getStation();
  }

  /*
   * (non-Javadoc)
   * 
   * @see
   * nl.cmkprojects.sage.slimplayer.ISlimPlayer#getTracks(nl.cmkprojects.sage
   * .slimplayer.Playlist)
   */
  public Track[] getTracks(Playlist playlist) {
    return cmdCli.getTracks(playlist);
  }

  /*
   * (non-Javadoc)
   * 
   * @see nl.cmkprojects.sage.slimplayer.ISlimPlayer#getPlaylists()
   */
  public Playlist[] getPlaylists() {
    return cmdCli.getPlaylists();
  }

  private boolean isSqueezeCenterVersion() {
    String value = Config.getProperty("serverversion");
    if (value != null) {
      int major = Integer.parseInt(value.trim().substring(0, 1));
      if (major >= 7) {
        return true;
      }
    } else {
      return true;
    }
    return false;
  }

  /*
   * (non-Javadoc)
   * 
   * @see nl.cmkprojects.sage.slimplayer.ISlimPlayer#getFavorites()
   */
  public Favorite[] getFavorites() {
    logger.debug("getFavorites()");
    return cachedFavorites;
  }

  public static void main(String[] args) {
    SlimPlayerImpl sp = getInstance();

    try {
      Thread.sleep(30000);
      sp.powerOff();
      Thread.sleep(30000);
      sp.powerOn();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  private boolean isProtoConnected() {
    return squeeze != null ? squeeze.getProtocol().isConnected() : false;
  }

  /*
   * (non-Javadoc)
   * 
   * @see nl.cmkprojects.sage.slimplayer.ISlimPlayer#togglePower()
   */
  public void togglePower() {
    if (isPowerOn()) {
      powerOff();
    } else {
      powerOn();
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see nl.cmkprojects.sage.slimplayer.ISlimPlayer#powerOff()
   */
  public void powerOff() {
    logger.debug("powerOff");
    cmdCli.powerOff();
  }

  /*
   * (non-Javadoc)
   * 
   * @see nl.cmkprojects.sage.slimplayer.ISlimPlayer#powerOn()
   */
  public void powerOn() {
    logger.debug("powerOn");
    cmdCli.powerOn();
  }

  /*
   * (non-Javadoc)
   * 
   * @see nl.cmkprojects.sage.slimplayer.ISlimPlayer#powerOn()
   */
  public boolean isPowerOn() {
    logger.debug("powerOn");
    return cmdCli.isPowerOn();
  }

  public void cliConnected() {
    logger.debug("cli connected");
  }

  public void cliDisconnected() {
    logger.debug("cli disconnected");
  }

  public synchronized void cliMessage(CliMessage reply) {
    logger.debug("cliMessage received: " + reply.toString());
    // ignore other players

    boolean changed = false;
    CliParameterIterator iter = reply.getParameterIterator();
    if (reply.getCommand().equalsIgnoreCase("displaystatus")) {
      while (iter.hasNext()) {
        String tag = iter.nextTag();
        if (tag.equals("line0")) {
          String value = Util.urlDecode(iter.nextValue());
          changed = changed || !value.equals(displayLine0);
          displayLine0 = value;
        } else if (tag.equals("line1")) {
          String value = Util.urlDecode(iter.nextValue());
          changed = changed || !value.equals(displayLine1);
          displayLine1 = value;
        }
      }
    } else if (reply.getCommand().equalsIgnoreCase("status")) {
      logger.debug("cliMessage received:  status - 1 command");
      // status <start> <itemsPerResponse> <taggedParameters>
      iter.nextParameter(); // <start> 0
      iter.nextParameter(); // <itemsPerResponse>
      boolean isRemote = false;
      String artist = serverState.getArtist();
      while (iter.hasNext()) {
        String tag = iter.nextTag();
        if (tag.equals("remote")) {
          // remote stream is playing ?
          // for local streams this tag is not present hence the local variable
          // construct
          String value = iter.nextValue();
          isRemote = "1".equals(value);
        } else if (tag.equals("mode")) {
          String value = iter.nextValue();
          if (value != null) {
            serverState.setMode(value);
          }
        } else if (tag.equals("power")) {
          String value = iter.nextValue();
          serverState.setPowerOn("1".equals(value));
        } else if (tag.equals("mixer volume")) {
          String value = iter.nextValue();
          try {
            serverState.setVolume(Integer.parseInt(value));
          } catch (NumberFormatException e) {
            logger.error("failed to parse mixer volume", e);
          }
        } else if (tag.equals("current_title")) {
          String value = iter.nextValue();
          if (value != null) {
            serverState.setCurrentTitle(Util.urlDecode(value));
          }
        } else if (tag.equals("title")) {
          String value = iter.nextValue();
          if (value != null) {
            serverState.setTitle(Util.urlDecode(value));
          }
        } else if (tag.equals("artist")) {
          String value = iter.nextValue();
          if (value != null) {
            serverState.setArtist(Util.urlDecode(value));
          }
        } else if (tag.equals("mode")) {
          String value = iter.nextValue();
          if (value != null) {
            serverState.setMode(value);
          }
        } else if (tag.equals("time")) {
          String value = iter.nextValue();
          if (value != null) {
            try {
              serverState.setTime((int) Float.parseFloat(value));
            } catch (NumberFormatException e) {
              logger.error("failed to parse time", e);
            }
          }
        } else if (tag.equals("duration")) {
          String value = iter.nextValue();
          if (value != null) {
            try {
              serverState.setDuration((int) Float.parseFloat(value));
            } catch (NumberFormatException e) {
              logger.error("failed to parse duration", e);
            }
          }
        }
      }
      serverState.setRemote(isRemote);
      if (isRemote) {
        // for remote streams title tag will give station
        // name
        serverState.setStation(serverState.getCurrentTitle());
      } else {
        serverState.setStation("");
        serverState.setCurrentTitle(serverState.getTitle());
      }
      logger.debug("cliMessage received:  status 0 1: old artist= " + artist + ", new artist=" + serverState.getArtist());
      if (!artist.equals(serverState.getArtist())) {
        artistInfoManager.updateArtistInfo(serverState.getArtist(), serverState.getTitle());
      }
      cachedFavorites = cmdCli.getFavorites();
    }
    if (changed) {
      SageTVApi.refreshGuiArea(uiContext, "NowPlaying");
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see nl.cmkprojects.sage.slimplayer.ISlimPlayer#isPlaying()
   */
  public boolean isPlaying() {
    // return cmdCli.isPlaying();
    return serverState.isPlaying();
  }

  /*
   * (non-Javadoc)
   * 
   * @see nl.cmkprojects.sage.slimplayer.ISlimPlayer#getVersion()
   */
  public String getVersion() {
    return Version.getVersion();
  }

  public void ir(String code) {
    if (squeeze != null) {
      int jiffies = squeeze.getProtocol().getJiffies();
      cmdCli.ir(code, jiffies);
    }
  }

  private void registerCliListener() {
    logger.debug("registerCliListener()");
    CliMessage filter = new CliMessage(playerid, "status");
    listenCli.removeFilter(filter, this);
    listenCli.addFilter(filter, this);
    listenCli.queueMessage(null, new CliMessage(playerid, "displaystatus").addParameter("subscribe:all"));
    listenCli.queueMessage(null, new CliMessage(playerid, "status").addParameter("-").addParameter("1")
        .addParameter("subscribe:60"));
  }

  /*
   * (non-Javadoc)
   * 
   * @see nl.cmkprojects.sage.slimplayer.ISlimPlayer#getDisplayText()
   */
  public String getDisplayText() {
    logger.debug("getDisplayText()");
    retrieveDisplayText();
    return displayLine0 + "\n" + displayLine1;
  }

  private void retrieveDisplayText() {
    if (isProtoConnected()) {
      if (displayLine0.length() == 0) {
        String t[] = cmdCli.getDisplayText().split("\n");
        if (t.length > 0) {
          displayLine0 = t[0];
        }
        if (t.length > 1) {
          displayLine1 = t[1];
        }
      }
    }
  }
  public String getDisplayTextLine0() {
    logger.debug("getDisplayTextLine0()");
    retrieveDisplayText();
    return displayLine0;
  }
  public String getDisplayTextLine1() {
    logger.debug("getDisplayTextLine1()");
    retrieveDisplayText();
    return displayLine1;
  }

  public void button(String button) {
    logger.debug("sending CLI button: " + button);
    cmdCli.button(button);
  }

  public void button(int value) {
    logger.debug("sending SQUEEZE button: " + value);
    if (squeeze != null) {
      squeeze.sendButton(value);
    }
  }

  public void slimprotoCmd(String cmd, byte[] buf, int off, int len) {
    byte[] letter = new byte[5];
    if (buf.length >= 5) {
      System.arraycopy(buf, 0, letter, 0, 5);
      String pCmd = new String(letter);
      handleProtoCmd(pCmd);
    }
  }

  private AudioCollaboration getAudioCollaborationMode() {
    return AudioCollaboration.valueOf(getSlimProperty(AUDIO_COLLABORATION_PROPERTY_NAME, AudioCollaboration.EXCLUSIVE.toString()));
  }

  private String getLoopbackStationID() {
    return getSlimProperty(LOOPBACK_STATIONID, "");
  }

  private void setModeStop() {
    synchronized (this) {
      if (playMode == PlayMode.PLAY) {
        playMode = PlayMode.STOP;
        logger.debug("setModeStop - mode was playing");

        if (getAudioCollaborationMode().equals(AudioCollaboration.EXCLUSIVE)) {
          // clearInfo();
          setSlimProperty(DISPLAY_VIDEO_ON_MENUS_PROP, displayVideoOnMenus);
        }
      }
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see nl.cmkprojects.sage.slimplayer.ISlimPlayer#enableAutoTransitions(java
   * .lang.String, java.lang.String, java.lang.String)
   */
  public void enableAutoTransitions(String sourceMenu, String destinationMenu, String enabled) {
    logger.debug("enableAutoTransitions(" + sourceMenu + ", " + destinationMenu + ")");
    if (transitionTimerTask == null) {
      transitionTimerTask = new AutoTransitionTimerTask(uiContext, sourceMenu, destinationMenu, Boolean.parseBoolean(enabled));
    } else {
      transitionTimerTask.setEnabled(Boolean.parseBoolean(enabled));
      transitionTimerTask.setUiContext(uiContext);
      transitionTimerTask.setSourceMenu(sourceMenu);
      transitionTimerTask.setNextMenu(destinationMenu);
    }
  }

  public void enterNavigationMode() {

  }

  public void leaveNavigationMode() {
    listenCli.queueMessage(null, new CliMessage(playerid, "displaystatus").addParameter("subscribe:all"));
  }

  private void stopNowPlayingMenuTransitionTimer() {
    logger.debug("stopNowPlayingMenuTransitionTimer()");
    if (transitionTimerTask != null) {
      transitionTimerTask.cancel();
    }
  }

  private void startNowPlayingMenuTransitionTimer() {
    if (transitionTimerTask != null) {
      logger.debug("startNowPlayingMenuTransitionTimer()");
      if (transitionTimerTask != null) {
        transitionTimerTask.cancel();
        transitionTimerTask.start();
      }
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see
   * nl.cmkprojects.sage.slimplayer.ISlimPlayer#handleMediaPlayerFullyLoaded()
   */
  public void handleMediaPlayerFullyLoaded() {
    AudioCollaboration col = getAudioCollaborationMode();
    logger.debug("handleMediaPlayerFullyLoaded - mode=" + playMode + ", col-mode=" + col);
    if (playMode == PlayMode.PLAY) {
      if (col.equals(AudioCollaboration.EXCLUSIVE)) {
        playMode = PlayMode.STOP;
        stopPlayback();
      } else if (col.equals(AudioCollaboration.SYNC)) {
        /*
         * If other channel than loopback channel was started then stop
         * slimplayer
         */
        String loopbackDevice = getLoopbackDeviceInput();
        String viewingDevice = SageTVApi.getCaptureDeviceInputBeingViewed(uiContext);
        logger.debug("viewing device input = " + viewingDevice + ", loopback device input = " + loopbackDevice);
        if (!loopbackDevice.equals(viewingDevice)) {
          playMode = PlayMode.STOP;
          stopPlayback();
        } else {
          // seek to end of file
          SageTVApi.watchSeekToLive(uiContext);
        }
      }
    }
  }

  private String getLoopbackDeviceInput() {
    return getSlimProperty(LOOPBACK_DEVICE_INPUT, "");
  }

  private void setModePlay() {
    synchronized (this) {
      if (playMode == PlayMode.STOP) {
        playMode = PlayMode.PLAY;
        logger.debug("setModePlay - mode was stop");
        // for each connected sagetv UIContext
        if (getAudioCollaborationMode().equals(AudioCollaboration.EXCLUSIVE)) {
          logger.debug("col-mode=exclusive");
          displayVideoOnMenus = getSlimProperty(DISPLAY_VIDEO_ON_MENUS_PROP, DISPLAY_VIDEO_ON_MENUS_XNEVER);
          setSlimProperty(DISPLAY_VIDEO_ON_MENUS_PROP, DISPLAY_VIDEO_ON_MENUS_XNEVER);
          SageTVApi.stopSageTvMediaPlayer(uiContext);
        }
      }
    }
  }

  private void handleProtoCmd(String cmd) {
    logger.debug("cmd: " + cmd);
    if (cmd.equals("strms")) {
      setModePlay();
    } else if (cmd.equals("strmu")) {
      setModePlay();
    } else if (cmd.equals("strmp")) {
      setModeStop();
    } else if (cmd.equals("strmq")) {
      setModeStop();
    } else if (cmd.equals("strmt")) {
      // force update of artistinfos
      // updateInfo(null);
    }
  }

  public void slimprotoConnected() {
    logger.info("slimproto connection established");
  }

  public void slimprotoDisconnected() {
    logger.info("slimproto disconnected");
    setModeStop();
  }

  public void reconnect() {
    try {
      initSqueeze();
    } catch (Exception e) {
      logger.error("failed to reconnect softsqueeze", e);
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see nl.cmkprojects.sage.slimplayer.ISlimPlayer#isMuted()
   */
  public boolean isMuted() {
    return cmdCli.isMuted();
  }

  /*
   * (non-Javadoc)
   * 
   * @see nl.cmkprojects.sage.slimplayer.ISlimPlayer#mute(boolean)
   */
  public void mute(boolean muted) {
    if (muted != isMuted()) {
      if (muted) {
        mutedVolume = serverState.getVolume();
      }
      cmdCli.muteToggle();
      if (!muted) {
        // we have to wait two second here, before changing the volume
        try {
          Thread.sleep(2000);
        } catch (InterruptedException e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
        }
        cmdCli.setVolume(mutedVolume);
      }
    }
  }

  private Map<String, String> getSongInfoByTrackId(String id) {
    logger.debug("getSongInfoByTrackId(" + id + ")");
    return cmdCli.getSongInfo("track_id", id);
  }

  private Map<String, String> getSongInfoByUrl(String path) {
    logger.debug("getSongInfo(" + path + ")");
    return cmdCli.getSongInfo("url", path);
  }

  public String getCoverArtUrl(Playable playable) {
    Map<String, String> songInfo = getSongInfo(playable);
    if (songInfo != null) {
      return songInfo.get("CoverartUrl");
    }
    return "";
  }

  /*
   * (non-Javadoc)
   * 
   * @see nl.cmkprojects.sage.slimplayer.ISlimPlayer#getSongInfo(nl.cmkprojects
   * .sage.slimplayer.Playable)
   */
  @SuppressWarnings("unchecked")
  public Map<String, String> getSongInfo(Playable playable) {
    logger.debug("getSongInfo(" + playable + ")");
    Map<String, String> songInfo = new HashMap<String, String>();
    if (playable == null) {
      String path = cmdCli.getPath();
      String title = cmdCli.getCurrentTitle();
      if (StringUtils.isNotEmpty(path)) {
        songInfo = (Map<String, String>) songInfoCache.get(title);
        if (songInfo == null) {
          songInfo = getSongInfoByUrl(path);
          if (songInfo != null) {
            songInfoCache.put(title, songInfo);
          }
        }
      }
    } else {
      if (playable instanceof Track) {
        Track track = (Track) playable;
        songInfo = getSongInfoByTrackId(track.getId());
      } else if (playable instanceof Playlist) {
        Playlist pl = (Playlist) playable;
        songInfo = getSongInfoByTrackId(pl.getId());
      } else if (playable instanceof Favorite) {
        Favorite fav = (Favorite) playable;
        songInfo = getSongInfoByUrl(fav.getUrl());
      }
    }
    return songInfo;
  }

  /*
   * (non-Javadoc)
   * 
   * @see nl.cmkprojects.sage.slimplayer.ISlimPlayer#getTrackInfo(nl.cmkprojects
   * .sage.slimplayer.Playable, java.lang.String)
   */
  public String getTrackInfo(Playable playable, String filter) {
    Map<String, String> map = getSongInfo(playable);
    StringBuffer sb = new StringBuffer();
    String[] keys = getSongInfoKeys(map, filter);
    for (int i = 0; i < keys.length; i++) {
      sb.append(keys[i] + ": " + map.get(keys[i]));
      if (i < keys.length - 1) {
        sb.append("\n");
      }
    }
    return sb.toString();
  }

  /*
   * (non-Javadoc)
   * 
   * @see
   * nl.cmkprojects.sage.slimplayer.ISlimPlayer#getSongInfoKeys(java.util.Map,
   * java.lang.String)
   */
  public String[] getSongInfoKeys(Map<String, String> songInfo, String filter) {
    List<String> headers = new ArrayList<String>();
    if (songInfo != null) {
      if (filter != null) {
        String[] keys = filter.split(",");
        for (int i = 0; i < keys.length; i++) {
          String key = keys[i];
          String val = (String) songInfo.get(key);

          if (val != null) {
            if (key.equals("Composer") && val.length() <= 1) {
              continue;
            }
            if (key.equals("Band") && val.length() <= 1) {
              continue;
            }
            headers.add(key);
          }
        }
      } else {
        Iterator<String> iter = songInfo.keySet().iterator();
        while (iter.hasNext()) {
          String key = (String) iter.next();
          headers.add(key);
        }
      }
    }

    return (String[]) headers.toArray(new String[0]);
  }

  /*
   * (non-Javadoc)
   * 
   * @see nl.cmkprojects.sage.slimplayer.ISlimPlayer#getVersionInfo()
   */
  public String getVersionInfo() {
    StringBuffer sb = new StringBuffer();
    sb.append("SlimPlayer by koelec, version " + Version.getSlimPlayerVersion());
    sb.append("\n");
    sb.append("Stvi version=" + stviVersion);
    sb.append("\n");
    sb.append("softsqueezeversion=" + Config.getProperty("softsqueezeversion"));
    sb.append("\n");
    sb.append("serverversion=" + Config.getProperty("serverversion"));
    sb.append("\n");
    sb.append("audio.mixer=" + Config.getProperty(AUDIO_MIXER));
    sb.append("\n");
    sb.append("audio.mp3decoder=" + Config.getProperty(AUDIO_MP3DECODER));
    sb.append("\n");
    sb.append("audio.lineBufferSize=" + Config.getProperty("audio.lineBufferSize"));
    sb.append("\n");
    sb.append("slimserver=" + Config.getProperty("slimserver"));
    sb.append("\n");
    sb.append("httpport=" + Config.getProperty("httpport"));
    sb.append("\n");
    sb.append("slimport=" + Config.getProperty("slimport"));
    sb.append("\n");
    sb.append("macaddress=" + Config.getProperty(MACADDRESS));
    sb.append("\n");
    sb.append("audio.tcpwindowsize=" + Config.getProperty("audio.tcpwindowsize"));
    sb.append("\n");
    sb.append("sshtunnel=" + Config.getBooleanProperty("sshtunnel"));
    sb.append("\n");
    sb.append("sshport=" + Config.getProperty("sshport"));
    sb.append("\n");
    return sb.toString();
  }
}
